package com.hmdp.utils;


import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;


/**
 *
 缓存工具封装
 */
@Slf4j
@Component
public class CacheClient {

    //注入RedisTemplate---构造方法
    private final StringRedisTemplate stringRedisTemplate;
    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

//    基于StringRedisTemplate封装一个缓存工具类，满足下列需求:

//    方法1:将任意Java对象序列化为json并存储在string类型的key中，并且可以设置TTL过期时间
    public void set (String key,Object value ,Long time, TimeUnit unit)
    {
        //写入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
    }

//    方法2:将任意Java对象序列化为json并存储在string类型的key中，并且可以设置逻辑过期时间，用于处理缓存击穿问题
    public void setWithLogicalExpire (String key,Object value ,Long time, TimeUnit unit)
    {
        //创建RedisData对象封装数据
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));

        //写入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData),time,unit);
    }


//    方法3:根据指定的kev查询缓存，并反席列化为指定类型，利用缓存空值的方式解决缓存穿透问题
        public <R,ID> R getqueryWithPassThrough(String keyPrefix,
                                             ID  id, Class<R> type,
                                             Function<ID,R> dbFallback,
                                             Long time, TimeUnit unit)
        {
            //设置存入Redis中shop的key
            String key=keyPrefix + id;

            //1、根据id从Redis查询商铺详情
            String stringShopInfo = stringRedisTemplate.opsForValue().get(key);
            //2、判断Redis中是否存在商铺信息
            //3、存在，则直接返回
            if (StrUtil.isNotBlank(stringShopInfo)) {
                //6、则使用JSONUtil.toBean()方法将stringShopInfo转为Shop对象
                return JSONUtil.toBean(stringShopInfo, type);
            }

            //如果在Redis中查询到数据是空字符串，则说明缓存被穿透了，阻止访问到数据库
            if (stringShopInfo != null)
            {
                return null;
            }

            //4、不存在，则从数据库中查询--Mybatis Plus
            R r1 =dbFallback.apply( id);

            //5、判断数据库中是否存在
            if (r1 == null)
            {
                //往Redis中写入空字符窜，防止别有用心的人利用缓存穿透--占用大量无效内存
                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                //、不存在，则返回错误404
                return null;
            }

            //7、存在，则写入Redis中
           this.set(key,r1,time,unit);

            return r1;
        }


//    方法4:根据指定的key查询缓存，并反序列化为指定类型，需要利用逻辑过期解决缓存击穿问题

    //创建线程池----开十个线程玩玩
    private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);

    public <R,ID> R getqueryWithLogicalExpire(String keyPrefix,ID  id,
                                           Class<R> type,Function<ID,R> dbFallback,
                                           Long time, TimeUnit unit)
    {
        //设置存入Redis中shop的key
        String key=keyPrefix + id;

        //1、根据id从Redis查询商铺详情
        String ShopInfo = stringRedisTemplate.opsForValue().get(key);
        //2、判断Redis中是否存在商铺信息
        if (StrUtil.isBlank(ShopInfo)) {
            //3、不存在，则直接null--一般不会是空的
            return null;
        }

        //4、命中，把JSON反序列化转为对象===存的时候是什么类型取的时候就得是什么类型
        RedisData redisData = JSONUtil.toBean(ShopInfo, RedisData.class);
        //强转为我们所需的JSONObject类型
        JSONObject data = (JSONObject) redisData.getData();
        //5、转为店铺对象
        R r = JSONUtil.toBean(data,type);
        LocalDateTime expireTime = redisData.getExpireTime();
        //5、判断时间是否过期
        if (expireTime.isAfter(LocalDateTime.now()))//看看过期时间是否在当前时间之后
        {
            //5.1未过期直接返回商铺信息
            return r;

        }


        //===============缓存重建============
        //6过期了尝试获取互斥锁
        String keyLock = LOCK_SHOP_KEY + id;
        boolean tryLock = tryLock(keyLock);
        //6.1判断是否获取锁成功
        if (tryLock)
        {
            if (expireTime.isAfter(LocalDateTime.now()))
            {
                //看看过期时间是否在当前时间之后
                //5.1未过期直接返回商铺信息
                return r;
            }
            //判断再次判断一下Redis中对应缓存是否过期--如果存在则无需执行下面的缓存重建--做双重检测
            //因为有可能上一个线程刚刚释放完锁，当前线程立马获取锁成功，则会再一次进行缓存重建

            //7、成功获取锁开启独立线程缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    log.info("开启独立线程缓存重建==基于逻辑过期");
                    //缓存重建
                    //1、查询数据库
                    R r1 = dbFallback.apply(id);//函数式编程
                    //2、写入缓存
                    this.setWithLogicalExpire(key,r1,time,unit);

                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                finally {
                    //释放锁
                    unLock(keyLock);
                }
            });
        }

        //不管是否成功获取锁都要返回店铺数据
        return r;
    }



    /**
     *使用Redis中String类型的setenx方法获取自定义锁
     */
    public boolean tryLock(String key)
    {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        //不能直接返回，需要判断flag是否为空
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     * @param key
     */
    public void unLock(String key)
    {
        stringRedisTemplate.delete(key);
    }



}


